14. Iterator, Generator.md
本文是异步编程(上),内容主要包括:
- 迭代器 iterator
- 可迭代对象 iterable object
- 生成器 generator / yield
0 引子
简要介绍异步编程相关的知识点:
1 迭代相关
Iterator:一个迭代器包含了一个 next() 方法,它可以循环迭代出所在对象的所有值。
iterable object:当一个对象包含 iterator 迭代器的对象,同时拥有一个 Symbol.iterator 方法后,就可以利用 for...of 自动,或 it.next() 手动迭代。
generator / yield: 是一个生成器,通过这个生成器函数,我们可以方便的随时暂停 / 重启一个函数的执行,同时建立了一个和调用者双向传递的通道,可以多次的传入 / 传出值。generator 的实现原理,就是利用了 iterator 迭代器。当生成器函数在 yield 处暂停后,会处于暂时挂起状态,其函数本身依然在调用栈中存在。
2 异步相关
promise:优化了异步编程的形式,传统依赖回调的异步编程,经常会出现回调地狱。采用使用 promise 后,就有了 then 链,使异步编程的难度得到缓解。
async / await:是一个进一步优化的异步编程的形式,它本质是一个 promise 和 p.then() 的语法糖,优化了 then 链的编程方式。可以集中处理 rejected promise、更专注于对 promise 成功回调后的处理。
3 迭代 + 异步
promise + generator:实现了 generator 的异步迭代能力。
async generator:实质上是 promise + generator 的优化版本。promise 令 generator 拥有了异步迭代的能力。也就是 generator 每次迭代都是异步获取值的,一旦获取到值,就自动的把值返回给调用者。调用者可以利用在 async 环境中的 for await ...of 来遍历出 async generator 异步获取的值。
4 举例:
需求:模拟向服务器依次发送多个请求。
- 第一次发送:
"why",收到信息res = "why"; - 第二次发送:
res + "aaa",收到信息res = "whyaaa"; - 第三次发送:
res + "bbb",收到信息res = "whyaaabbb"; - 第四次发送:
res + "ccc",收到信息res = "whyaaabbbccc";
请求逻辑:
function requestData(url) {
// 异步请求的代码会被放入到 executor 中
return new Promise((resolve, reject) => {
setTimeout(() => { // 模拟网络请求
resolve(url); // 模拟受到请求,并返回请求结果
}, 2000);
});
}
1. promise 回调
- 传统使用会有回调地狱问题。
requestData("why").then((res) => {
requestData(res + "aaa").then((res) => {
requestData(res + "bbb").then((res) => {
console.log(res); // 'whyaaabbb'
});
});
});
2. promise 链
- promise 的 then 链,采用延迟绑定回调函数的方式,避免了回调地狱。
requestData("why")
.then((res) => {
return requestData(res + "aaa");
})
.then((res) => {
return requestData(res + "bbb");
})
.then((res) => {
console.log(res); // 'whyaaabbb'
});
3. Promise + generator
- 利用生成器可暂停 / 继续函数执行,并实现双向通信的特点,改变 then 链的调用形式。
把 then 调用链封装起来,程序员对业务逻辑的书写更符合直觉。
function* getData() {
const res1 = yield requestData("why");
const res2 = yield requestData(res1 + "aaa");
const res3 = yield requestData(res2 + "bbb");
const res4 = yield requestData(res3 + "ccc");
console.log(res4);
}
使用 then 调用链的方式:
3.1 如果要手工调用:
const generator = getData();
generator.next().value.then((res) => {
generator.next(res).value.then((res) => {
generator.next(res).value.then((res) => {
generator.next(res); // 'whyaaabbbccc'
});
});
});
3.2 通过递归,把重复逻辑封装,实现递归调用:
execGenerator(getData); // 'whyaaabbbccc'
function execGenerator(genFn) {
const generator = genFn();
exec();
function exec(res) {
const result = generator.next(res);
// 遍历完毕,递归结束
if (result.done) return result.value;
result.value.then((res) => {
exec(res); // 递归调用
});
}
}
3.3 exec 自动化方法,有现成的包:
co,由 TJ 编写的,已经实现了自动化执行。利用 co,程序员只关心业务逻辑(getData())即可。
TJ 还开发了:
co、n(类似 nvm)
commander(vue cil 使用过)
express(服务器框架)
koa(express 的升级版,另外目前基于 koa 的 egg 也比较流行)
const co = require('co')
co(getData);
4. promise + async/await
- ES2017 特性,官方引入了 promise + generator 思路,形成 async/await 结构。
async function getData() {
const res1 = await requestData("why");
const res2 = await requestData(res1 + "aaa");
const res3 = await requestData(res2 + "bbb");
const res4 = await requestData(res3 + "ccc");
console.log(res4);
}
getData();
1 Async / await 异步编程
async (asynchronous) 异步、psync (synchronous) 同步。
async function foo() {
return 1
}
如果函数前加入了 async 关键字,这个函数就成为了一个 Async。Async 通常和 Promise 配合使用,Async 进一步优化了 promise 的 then 调用链,使异步编程更舒适。
执行中:
- 异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。
- 异步函数内部,如果出现异常,并不影响函数外部作用域,而是会在异常处立即返回一个已决议的 rejected promise,并把异常原因返回。
返回值:
async函数的返回值,永远是一个promise,且该promise是已决议的。如果
return的是一个立即值,则包装为一个 rsolved promise 后返回;如果
return的是一个promise,则直接返回这个promise。如果
return的是一个 thenable,则会展开 (执行then()),得到一个立即值或promise。如果没有手动
return,且没有错误的情况下, 默认返回一个值为undefined的 resolved promise。如果没有手动
return,且有reject,如果没有用try...catch捕获,就会返回这个rejected promise。
3.1 await
和 generator / yield 类型,async / await 也是相互的搭档。await 只能在 async 中使用。
await... 表达式是一个暂停点,其右侧是一个 promise。当程序执行到 await... 时,await 会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
完成自动解析。
await实际上就是p.then()的语法糖(参考0.4 举例)。await会把右侧的promise通过.then( res => res)把 res 提取出来,返回给await的左侧。- 所以,如果
await后是一个立即值,则直接返回这个立即值。
- 所以,如果
错误顺延抛出。
await只负责解析 resolved 的 promise,如果 promise 是一个 rejected 的,那当前 async 函数会立即 rejected,把 await 处的 rejeted promise 值,当作 async rejected 的值抛出。- asnyc function 的返回值也是一个 promise。
- 如果不想顺延抛出,需要通过 try ... catch 捕获 await 右侧那个 rejected promise。
3.2 Error 处理
如果一个 promise 正常 fulfilled,await promise 返回的就是其结果,是一个已决议的 promise。
如果这个 promise 是 rejected,它将 throw 这个 error ,控制台中会显示一个未处理的 promise error:
async function foo() {
let p = await Promise.reject("错误")
return p
}
foo() // 错误
// Uncaught (in promise) 错误
try...catch
使用 try...catch 可以把 await 等待的 promise 中发生的错误捕获:
async function foo() {
try {
let p = await Promise.reject("错误")
} catch (e) {
console.log(e)
}
}
foo() // 错误
如果没有 await 等待,promise 中发生的错误就只在 promise 中抛出错误,需要 p.then() 或 p.catch() 去接收错误,无法使用 try...catch 捕获错误:
async function foo() {
try {
let p = Promise.reject("错误")
} catch (e) {
console.log(e)
}
}
foo() // Uncaught (in promise) 错误
如果不用 try..catch 去捕获 promise 中的错误,那么也可以在外部调用 async 函数的时候,添加一个 .catch() 作为“兜底”,处理 async 中可能出现的错误。
这就相当于, 异步函数 foo() 最终返回了一个 rejected 的 promise:
async function foo() {
let p = await Promise.reject("错误")
}
foo().catch(e => {
console.log(e) // 错误
})
所以,async / await 是 promise / p.then() 的语法糖。
await 解决了 then() 长长的一条链条,我们把 then 中 rejected 失败回调函数放到了 try...catch 中统一处理。然后 await 就只需等待正确的 resolve promise 了。
4 async generator 异步迭代
声明一个异步迭代函数,用 async
异步迭代允许我们对按需通过异步请求而得到的数据进行迭代。例如,我们通过网络分段(chunk-by-chunk)下载数据时。异步生成器(generator)使这一步骤更加方便。
4.1 异步的可迭代对象 async iterable object
我们知道,要使一个对象可迭代,我们需要给它添加 [Symbol.iterator]。
现象,如果令一个对象可异步迭代,需要添加 [Symbol.asyncIterator]。
当迭代的值通过异步形式输出时,iterator 的定义和使用产生了以下变化:
- 引擎不再调用
[Symbol.iterator]函数来迭代,而是使用[Symbol.asyncIterator]。 await it.next()方法会返回一个 fulfilled promise,且这个promise携带有返回值 IteratorResult。for...of应该使用异步迭代:for await (let v of iterable)。
for...of 通过调用异步可迭代对象的 [Symbol.iterator] 函数,获得一个包含 async next() 方法的对象。然后每次调用 async next() 都会获得一个包含结果值 IteratorResult 的 promise。
下面是一个每个 1000ms 后,输出一个数字的例子,采用异步的可迭代对象:
let number = {
array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
[Symbol.asyncIterator]() {
return {
array: this.array,
index: 0,
async next() {
let num = await new Promise((resolve) => {
setTimeout(() => {
resolve(this.array[this.index++])
}, 1000)
});
return {
done: num === undefined ? true : false,
value: num
}
}
}
}
};
// 必须包含在一个 async 中,才能使用 await
(async () => {
for await (let value of number) { // (4)
console.log(value) // 1, 3, 5, 7, 9, 11, 13, 15, 17, 19
}
})()
4.2 async generator 异步生成器
当我们想创建一个 异步生成一系列值的对象 时,我们都可以使用异步 generator。
async function* gen() {
for (let i = 0, i < 20; i++) {
await new Promise(reslove => setTimeout(reslove, 1000));
yield i;
}
};
// for await ...of 遍历
(async() => {
let it = gen();
for await(let v of it) {
console.log(v) // 间隔 1s,输出0 ~ 19
}
})();
定义异步 generator 时,注意几个要点:
- 定义异步 generator :
async function*; - 在异步 generator 中通过
await等待,来异步获取值; - 在异步 generator 中通过
yield来和调用者进行双向交互,把自己获取的值传递出去。
使用异步 generator 时,注意几个要点:
- 要异步环境中创建并调用使用启动器,创建一个
async环境的 IIFE; - 通过
for await ...of来逐次的获取异步值。- 使用
for await ...of原因是,异步值时通过await it.next()获取的。
- 使用
// 上面的程序进行手动遍历
// 运行时输入:
(async() => {
let it = gen();
console.log(await it.next())
console.log(await it.next())
console.log(await it.next())
console.log(await it.next())
console.log(await it.next())
})();
// 控制台依次输出:
// {value: 0, done: false}
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: 4, done: false}
4.3 async [Symbol.asyncIterator]
使用异步的 [Symbol.asyncIterator],来替换同步的 [Symbol.asyncIterator]
let range = {
from: 1,
to: 5,
// 这一行等价于 [Symbol.asyncIterator]: async function*() {
async *[Symbol.asyncIterator]() {
for(let value = this.from; value <= this.to; value++) {
// 在 value 之间暂停一会儿,模拟异步获取值
await new Promise(resolve => setTimeout(resolve, 1000));
// 通过 yield 返回值,而不再用 next() 方法
yield value;
}
}
};
(async () => {
for await (let value of range) {
console.log(value); // 间隔 1s 依次输出:1, 2, 3, 4, 5
}
})();
4.4 总结
异步 iterator 与常规 iterator 在语法上的区别:
| Iterator | async iterator | |
|---|---|---|
| 提供 iterator 的对象方法: | Symbol.iterator | Symbol.asyncIterator |
next() 返回的值是: | IteratorResult | promise,决议值是 IteratorResult |
| 要进行循环,使用: | for..of | for await..of |
异步 generator 函数与常规 generator 函数在语法上的区别:
| Generator | 异步 Generator | |
|---|---|---|
| 声明方式: | function* gen(){..} | async function* gen(){..} |
next() 返回的值是 | IteratorResult | promise,决议值是 IteratorResult |
| 要进行循环,使用: | for..of | for await..of |
应用背景:
在 Web 开发中,经常会遇到数据流,它们分段流动(flows chunk-by-chunk)。例如,下载或上传大文件。
此时,我们可以使用异步 generator 来处理此类数据。
值得注意的是,浏览器环境下,还有一个被称为 Streams 的 API,它提供了特殊的接口来处理此类数据流,转换数据、并将数据从一个数据流传递到另一个数据流(例如,从一个地方下载并立即发送到其他地方)。